/**
 * chenuu.com
 * 
 * @author  zhaoxu.com@gmail.com
 * @created 2012-11-10
 */
package com.chenuu.integration.dropbox.api;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.SyncFailedException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Service;

import com.chenuu.integration.dropbox.model.AccessType;
import com.chenuu.integration.dropbox.model.DropboxAccount;
import com.chenuu.integration.dropbox.model.DropboxEntry;
import com.chenuu.integration.dropbox.oauth.token.AccessToken;
import com.chenuu.integration.dropbox.oauth.token.OAuthToken;
import com.chenuu.integration.dropbox.oauth.token.RequestToken;
import com.chenuu.integration.dropbox.requester.DropboxRequester;
import com.chenuu.integration.dropbox.requester.RequestInfo;
import com.chenuu.integration.dropbox.requester.UploadInfo;
import com.chenuu.integration.dropbox.session.DefaultDropboxSession;
import com.chenuu.integration.dropbox.session.DropboxSession;
import com.chenuu.integration.httpclient.RequestMethod;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.exception.DropboxIOException;
import com.dropbox.client2.exception.DropboxLocalStorageFullException;
import com.dropbox.client2.exception.DropboxParseException;
import com.dropbox.client2.exception.DropboxPartialFileException;
import com.dropbox.client2.exception.DropboxServerException;
import com.dropbox.client2.exception.DropboxUnlinkedException;

/**
 *
 */
@Service("dropboxAPI")
public class DefaultDropboxAPI implements DropboxAPI {

	protected static final int METADATA_DEFAULT_LIMIT = 25000;

	@Override
	public OAuthToken requestToken() throws DropboxException {
		RequestInfo info = new RequestInfo(RequestMethod.GET,
				"https://api.dropbox.com/1/oauth/request_token");
		DropboxRequester requester = new DropboxRequester(new DefaultDropboxSession());
		HttpResponse response = requester.execute(info);
		Map<String, String> result = parseAsQueryString(response);
		return new RequestToken(result.get("oauth_token"), result.get("oauth_token_secret"));
	}

	@Override
	public String authorize(OAuthToken requestToken) {
		return "https://www.dropbox.com/1/oauth/authorize?oauth_token=" + requestToken.getKey()
				+ "&oauth_callback=http://chenuu.com?ttt";
	}

	@Override
	public OAuthToken accessToken(DropboxSession session) throws DropboxException {
		RequestInfo info = new RequestInfo(RequestMethod.GET,
				"https://api.dropbox.com/1/oauth/access_token");
		DropboxRequester requester = new DropboxRequester(session);
		HttpResponse response = requester.execute(info);
		Map<String, String> result = parseAsQueryString(response);
		return new AccessToken(result.get("oauth_token"), result.get("oauth_token_secret"));
	}

	@Override
	public DropboxAccount accountInfo(DropboxSession session) throws DropboxException {
		RequestInfo info = new RequestInfo(RequestMethod.GET,
				"https://api.dropbox.com/1/account/info");
		DropboxRequester requester = new DropboxRequester(session);
		HttpResponse response = requester.execute(info);
		Map<String, Object> result = parseAsJSON(response);
		return new DropboxAccount(result);
	}

	@Override
	public DropboxEntry metadata(DropboxSession session, AccessType accessType, String dropboxPath,
			int fileLimit, String hash, boolean list) throws DropboxException {

		if (fileLimit <= 0) {
			fileLimit = METADATA_DEFAULT_LIMIT;
		}

		String uri = "https://api.dropbox.com/1/metadata/" + accessType + dropboxPath;

		RequestInfo info = new RequestInfo(RequestMethod.GET, uri);
		info.addParam("file_limit", fileLimit);
		info.addParam("list", list);
		if (hash != null) {
			info.addParam("hash", hash);
		}

		DropboxRequester requester = new DropboxRequester(session);
		HttpResponse response = requester.execute(info);
		Map<String, Object> result = parseAsJSON(response);
		return new DropboxEntry(result);
	}

	@Override
	public void download(DropboxSession session, AccessType accessType, String dropboxPath,
			String localPath) throws DropboxException {

		String uri = "https://api-content.dropbox.com/1/files/" + accessType + dropboxPath;

		RequestInfo info = new RequestInfo(RequestMethod.GET, uri);
		DropboxRequester requester = new DropboxRequester(session);
		HttpResponse response = requester.execute(info);
		HttpEntity entity = response.getEntity();
		if (entity == null) {
			throw new DropboxException("Didn't get entity from HttpResponse");
		}

		try {
			InputStream is = entity.getContent();
			File file = new File(localPath);
			if (file.exists()) {
				return;
			} else {
				if (!file.getParentFile().exists()) {
					file.getParentFile().mkdirs();
				}
				file.createNewFile();
			}
			OutputStream os = new FileOutputStream(file);
			this.copyStreamToOutput(is, os);
		} catch (IOException e) {
			throw new DropboxIOException(e);
		}
	}

	/*
	 * Maximum file size limit of 150 MB.
	 * 
	 * @see com.chenuu.integration.dropbox.DropboxAPI#upload(com.chenuu.integration.dropbox.DropboxSession, com.chenuu.integration.dropbox.model.AccessType, java.lang.String, java.lang.String)
	 */
	@Override
	public void upload(DropboxSession session, AccessType accessType, String dropboxPath,
			String localPath) throws DropboxException {

		String uri = "https://api-content.dropbox.com/1/files_put/" + accessType + dropboxPath;

		UploadInfo info = new UploadInfo(RequestMethod.POST, uri, localPath);
		info.addParam("overwrite", false);

		DropboxRequester requester = new DropboxRequester(session);
		requester.execute(info);
	}

	private void copyStreamToOutput(InputStream is, OutputStream os) throws DropboxIOException,
			DropboxPartialFileException, DropboxLocalStorageFullException {

		BufferedOutputStream bos = null;
		long totalRead = 0;

		try {
			bos = new BufferedOutputStream(os);

			byte[] buffer = new byte[4096];
			int read;
			while (true) {
				read = is.read(buffer);
				if (read < 0) {
					break;
				}
				bos.write(buffer, 0, read);
				totalRead += read;
			}

			bos.flush();
			os.flush();
			// Make sure it's flushed out to disk
			try {
				if (os instanceof FileOutputStream) {
					((FileOutputStream) os).getFD().sync();
				}
			} catch (SyncFailedException e) {
			}

		} catch (IOException e) {
			String message = e.getMessage();
			if (message != null && message.startsWith("No space")) {
				// This is a hack, but it seems to be the only way to check
				// which exception it is.
				throw new DropboxLocalStorageFullException();
			} else {
				/*
				 * If the output stream was closed, we notify the caller
				 * that only part of the file was copied. This could have
				 * been because this request is being intentionally
				 * canceled.
				 */
				throw new DropboxPartialFileException(totalRead);
			}
		} finally {
			if (bos != null) {
				try {
					bos.close();
				} catch (IOException e) {
				}
			}
			if (os != null) {
				try {
					os.close();
				} catch (IOException e) {
				}
			}
			// This will also abort/finish the request if the download is
			// canceled early.
			try {
				is.close();
			} catch (IOException e) {
			}
		}
	}

	private Map<String, String> parseAsQueryString(HttpResponse response) throws DropboxException {

		HttpEntity entity = response.getEntity();

		if (entity == null) {
			throw new DropboxParseException("Bad response from Dropbox.");
		}
		Scanner scanner;
		try {
			scanner = new Scanner(entity.getContent()).useDelimiter("&");
		} catch (IOException e) {
			throw new DropboxIOException(e);
		}

		Map<String, String> result = new HashMap<String, String>();

		while (scanner.hasNext()) {
			String nameValue = scanner.next();
			String[] parts = nameValue.split("=");
			if (parts.length != 2) {
				throw new DropboxParseException("Bad query string from Dropbox.");
			}
			result.put(parts[0], parts[1]);
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	private Map<String, Object> parseAsJSON(HttpResponse response) throws DropboxException {
		Object result = null;

		BufferedReader bin = null;
		try {
			HttpEntity ent = response.getEntity();
			if (ent != null) {
				InputStreamReader in = new InputStreamReader(ent.getContent());
				// Wrap this with a Buffer, so we can re-parse it if it's
				// not JSON
				// Has to be at least 16384, because this is defined as the buffer size in
				//     org.json.simple.parser.Yylex.java
				// and otherwise the reset() call won't work
				bin = new BufferedReader(in, 16384);
				bin.mark(16384);

				JSONParser parser = new JSONParser();
				result = parser.parse(bin);
			}
		} catch (IOException e) {
			throw new DropboxIOException(e);
		} catch (ParseException e) {
			if (DropboxServerException.isValidWithNullBody(response)) {
				// We have something from Dropbox, but it's an error with no reason
				throw new DropboxServerException(response);
			} else {
				// This is from Dropbox, and we shouldn't be getting it
				throw new DropboxParseException(bin);
			}
		} catch (OutOfMemoryError e) {
			throw new DropboxException(e);
		} finally {
			if (bin != null) {
				try {
					bin.close();
				} catch (IOException e) {
				}
			}
		}

		int statusCode = response.getStatusLine().getStatusCode();
		if (statusCode != DropboxServerException._200_OK) {
			if (statusCode == DropboxServerException._401_UNAUTHORIZED) {
				throw new DropboxUnlinkedException();
			} else {
				throw new DropboxServerException(response, result);
			}
		}

		return (Map<String, Object>) result;
	}

}
